#include "tdp2_glrenderdevice.hpp"

using namespace TDP::Resources;
using namespace RDX;

namespace TDP
{
	namespace Rendering
	{
		template<class _T>
		inline _T *GLHandleToRef(LargeInt h)
		{
			return reinterpret_cast<_T *>(reinterpret_cast<UInt8 *>(NULL) + h);
		}

		
		static const GLenum GLPixelStoreSetting_TargetNames[] =
		{
			GL_PACK_ALIGNMENT,
			GL_UNPACK_ALIGNMENT,
		};

		static const GLenum GLTextureTarget_TargetNames[] =
		{
			GL_TEXTURE_2D,
			GL_TEXTURE_RECTANGLE,
			GL_TEXTURE_3D,
			GL_TEXTURE_CUBE_MAP,
		};

		static const GLenum GLTextureTarget_BindingNames[] =
		{
			GL_TEXTURE_BINDING_2D,
			GL_TEXTURE_BINDING_RECTANGLE,
			GL_TEXTURE_BINDING_3D,
			GL_TEXTURE_BINDING_CUBE_MAP,
		};
		
		static const GLenum GLRectSides[] =
		{
			GL_TEXTURE_RECTANGLE,
		};
		
		static const GLenum GL2DSides[] =
		{
			GL_TEXTURE_2D,
		};

		static const GLenum GL3DSides[] =
		{
			GL_TEXTURE_3D,
		};

		static const GLenum GLCubeSides[] =
		{
			GL_TEXTURE_CUBE_MAP_POSITIVE_X_ARB,
			GL_TEXTURE_CUBE_MAP_NEGATIVE_X_ARB,
			GL_TEXTURE_CUBE_MAP_POSITIVE_Y_ARB,
			GL_TEXTURE_CUBE_MAP_NEGATIVE_Y_ARB,
			GL_TEXTURE_CUBE_MAP_POSITIVE_Z_ARB,
			GL_TEXTURE_CUBE_MAP_NEGATIVE_Z_ARB,
		};

		void GLState::Initialize(GLFunctions *gl)
		{
			_gl = gl;

			for(int i=0;i<GLTT_Count;i++)
			{
				_textureBinding[i].target = GLTextureTarget_TargetNames[i];
				_textureBinding[i].binding = GLTextureTarget_BindingNames[i];
			}

			for(int i=0;i<GLPSS_Count;i++)
				_pixelStoreBindings[i].pname = GLPixelStoreSetting_TargetNames[i];
		}

		GLenum GLState::TextureTarget(GLTextureTarget target) const
		{
			return GLTextureTarget_TargetNames[target];
		}

		void GLState::BindTexture(GLTextureTarget target, GLuint name)
		{
			_textureBinding[target].value = name;
			_gl->BindTexture(TextureTarget(target), name);
		}

		void GLState::PixelStore(GLPixelStoreSetting pss, GLint value)
		{
			_pixelStoreBindings[pss].value = value;
			_gl->PixelStorei(_pixelStoreBindings[pss].pname, value);
		}

		void GLState::GetCurrent()
		{
			for(int i=0;i<GLTT_Count;i++)
			{
				GLint v;
				_gl->GetIntegerv(_textureBinding[i].binding, &v);
				_textureBinding[i].value = static_cast<GLuint>(v);
			}
			for(int i=0;i<GLPSS_Count;i++)
				_gl->GetIntegerv(_pixelStoreBindings[i].pname, &_pixelStoreBindings[i].value);
		}

		GLRenderDevice::GLRenderDevice(const RDX::ObjectManagement::Allocator &alloc, const IGLInitializer *initializer, const char **blame)
		{
			_startedOK = false;
			if(!_gl.Initialize(initializer, blame))
				return;
			_alloc = alloc;
			_state.Initialize(&_gl);
			_state.GetCurrent();
			_initialState = _state;

			_startedOK = true;
		}

		bool GLRenderDevice::CreateTexture(Resources::TextureSpec::TextureFormat tf, Resources::TextureSpec::TextureLayout tl, UInt32 numMipMaps,
				UInt32 width, UInt32 height, UInt32 depth, UInt32 flags, RenderTextureHandle *outRTH)
		{
			GLenum exformat = 0;
			GLenum exchannels = 0;
			GLenum iformat;
			GLTextureTarget target;
			unsigned int minWidth = 1;
			unsigned int minHeight = 1;
			unsigned int bpp = 0;
			bool compressed = false;
			bool threeD = false;
			const GLenum *sides;
			unsigned int numSides = 1;

			switch(tl)
			{
			case TextureSpec::TL_Cube:
				target = GLTT_TextureCube;
				sides = GLCubeSides;
				numSides = 6;
				break;
			case TextureSpec::TL_Single2D:
				target = GLTT_Texture2D;
				sides = GL2DSides;
				break;
			case TextureSpec::TL_SingleRect:
				target = GLTT_TextureRect;
				sides = GLRectSides;
				break;
			case TextureSpec::TL_Single3D:
				target = GLTT_Texture3D;
				sides = GL3DSides;
				threeD = true;
				break;
			default:
				return false;
			};

			switch(tf)
			{
			case TextureSpec::TF_DXT1:
				if(flags & TXF_sRGB)
					iformat = GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;
				else
					iformat = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
				exchannels = GL_RGB;
				exformat = GL_UNSIGNED_BYTE;
				minWidth = minHeight = 4;
				bpp = 4;
				compressed = true;
				break;
			case TextureSpec::TF_DXT3:
				if(flags & TXF_sRGB)
					iformat = GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT;
				else
					iformat = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
				exchannels = GL_RGBA;
				exformat = GL_UNSIGNED_BYTE;
				minWidth = minHeight = 4;
				bpp = 8;
				compressed = true;
				break;
			case TextureSpec::TF_DXT5:
				if(flags & TXF_sRGB)
					iformat = GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT;
				else
					iformat = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
				exchannels = GL_RGBA;
				exformat = GL_UNSIGNED_BYTE;
				minWidth = minHeight = 4;
				bpp = 8;
				compressed = true;
				break;
			case TextureSpec::TF_A8:
				if(flags & TXF_sRGB)
					iformat = GL_SLUMINANCE8_EXT;
				else
					iformat = GL_LUMINANCE;
				exchannels = GL_LUMINANCE;
				exformat = GL_UNSIGNED_BYTE;
				bpp = 8;
				break;
			case TextureSpec::TF_RGBA8:
				if(flags & TXF_sRGB)
					iformat = GL_SRGB_ALPHA_EXT;
				else
					iformat = GL_RGBA;
				exchannels = GL_RGBA;
				exformat = GL_UNSIGNED_BYTE;
				bpp = 32;
				break;
			case TextureSpec::TF_RGBA5551:
				if(flags & TXF_sRGB)
					iformat = GL_SRGB_ALPHA_EXT;
				else
					iformat = GL_RGBA;
				exchannels = GL_RGBA;
				exformat = GL_UNSIGNED_SHORT_5_5_5_1;
				bpp = 16;
				break;
			case TextureSpec::TF_RGBA4:
				if(flags & TXF_sRGB)
					iformat = GL_SRGB_ALPHA_EXT;
				else
					iformat = GL_RGBA;
				exchannels = GL_RGBA;
				exformat = GL_UNSIGNED_SHORT_4_4_4_4;
				bpp = 16;
				break;
			case TextureSpec::TF_RGB8:
				if(flags & TXF_sRGB)
					iformat = GL_SRGB_ALPHA_EXT;
				else
					iformat = GL_RGBA;
				exchannels = GL_RGBA;
				exformat = GL_UNSIGNED_BYTE;
				bpp = 24;
				break;
			case TextureSpec::TF_RGB565:
				if(flags & TXF_sRGB)
					iformat = GL_SRGB_ALPHA_EXT;
				else
					iformat = GL_RGBA;
				exchannels = GL_RGBA;
				exformat = GL_UNSIGNED_SHORT_5_6_5;
				bpp = 16;
				break;
			default:
				return false;
			}

			GLuint texID = 0;
			_gl.GenTextures(1, &texID);
			if(texID == 0)
				return false;

			GLTexture *tex = _alloc.CAlloc<GLTexture>(1);
			if(!tex)
			{
				_gl.DeleteTextures(1, &texID);
				return false;
			}

			tex->bpp = bpp;
			tex->exchannels = exchannels;
			tex->exformat = exformat;
			tex->iformat = iformat;
			tex->height = height;
			tex->width = width;
			tex->depth = depth;
			tex->threeD = threeD;
			tex->compressed = compressed;
			tex->target = target;
			tex->texID = texID;
			tex->sides = sides;
			tex->numSides = numSides;

			_state.BindTexture(target, texID);
			GLenum gltarget = _state.TextureTarget(target);

			// WARNING: Setting max LOD after uploads can crash some ATI drivers
			_gl.TexParameteri(gltarget, GL_TEXTURE_MAX_LEVEL, numMipMaps - 1);

			if(flags & TXF_AutoMipMap)
				_gl.TexParameteri(gltarget, GL_GENERATE_MIPMAP_SGIS, GL_TRUE);

			for(unsigned int side=0;side<numSides;side++)
			{
				UInt32 levelWidth = width;
				UInt32 levelHeight = height;
				UInt32 levelDepth = depth;
				for(UInt32 level=0;level<numMipMaps;level++)
				{
					if(level != 0)
					{
						levelWidth /= 2;
						levelHeight /= 2;
						levelDepth /= 2;
					}
					if(levelWidth < minWidth || levelHeight < minHeight || (threeD && levelDepth < 1))
					{
						_gl.DeleteTextures(1, &texID);
						_alloc.Free(tex);
						return false;
					}

					if(compressed)
					{
						if(threeD)
							_gl.CompressedTexImage3DARB(sides[side], level, iformat, levelWidth, levelHeight, levelDepth, 0, levelWidth * levelHeight * bpp / 8, NULL);
						else
							_gl.CompressedTexImage2DARB(sides[side], level, iformat, levelWidth, levelHeight, 0, levelWidth * levelHeight * bpp / 8, NULL);
					}
					else
					{
						if(threeD)
							_gl.TexImage3D(sides[side], level, iformat, levelWidth, levelHeight, levelDepth, 0, exchannels, exformat, NULL);
						else
							_gl.TexImage2D(sides[side], level, iformat, levelWidth, levelHeight, 0, exchannels, exformat, NULL);
					}
				}
			}

			*outRTH = reinterpret_cast<const UInt8 *>(tex) - reinterpret_cast<const UInt8 *>(NULL);
			return true;
		}

		void GLRenderDevice::UploadTextureRows(RenderTextureHandle rth, UInt32 level, UInt32 side, UInt32 y, UInt32 z, UInt32 numRows, const void *data, LargeUInt numBytes)
		{
			GLTexture *tex = GLHandleToRef<GLTexture>(rth);
			_state.BindTexture(tex->target, tex->texID);
			_state.PixelStore(GLPSS_UnpackAlignment, 1);
			GLenum err = _gl.GetError();

			if(tex->threeD)
			{
				if(tex->compressed)
					_gl.CompressedTexSubImage3DARB(tex->sides[side], static_cast<GLint>(level), 0, static_cast<GLint>(y), static_cast<GLint>(z), static_cast<GLint>(tex->width), static_cast<GLint>(numRows), 1,
						tex->iformat, static_cast<GLsizei>(numBytes), data);
				else
					_gl.TexSubImage3D(tex->sides[side], static_cast<GLint>(level), 0, static_cast<GLint>(y), static_cast<GLint>(z), static_cast<GLint>(tex->width), static_cast<GLint>(numRows), 1,
						tex->exchannels, tex->exformat, data);
			}
			else
			{
				if(tex->compressed)
					_gl.CompressedTexSubImage2DARB(tex->sides[side], static_cast<GLint>(level), 0, static_cast<GLint>(y), static_cast<GLint>(tex->width), static_cast<GLint>(numRows),
						tex->iformat, static_cast<GLsizei>(numBytes), data);
				else
					_gl.TexSubImage2D(tex->sides[side], static_cast<GLint>(level), 0, static_cast<GLint>(y), static_cast<GLint>(tex->width), static_cast<GLint>(numRows),
						tex->exchannels, tex->exformat, data);
				err = _gl.GetError();
			}

		}

		void GLRenderDevice::UploadTextureSubRow(RenderTextureHandle rth, UInt32 level, UInt32 side, UInt32 x, UInt32 y, UInt32 z, UInt32 width, const void *data, LargeUInt numBytes)
		{
			GLTexture *tex = GLHandleToRef<GLTexture>(rth);
			_state.BindTexture(tex->target, tex->texID);
			_state.PixelStore(GLPSS_UnpackAlignment, 1);

			if(tex->threeD)
			{
				if(tex->compressed)
					_gl.CompressedTexSubImage3DARB(tex->sides[side], static_cast<GLint>(level), static_cast<GLint>(x), static_cast<GLint>(y), static_cast<GLint>(z), static_cast<GLint>(tex->width), 1, 1,
						tex->iformat, static_cast<GLsizei>(numBytes), data);
				else
					_gl.TexSubImage3D(tex->sides[side], static_cast<GLint>(level), static_cast<GLint>(x), static_cast<GLint>(y), static_cast<GLint>(z), static_cast<GLint>(tex->width), 1, 1,
						tex->exchannels, tex->exformat, data);
			}
			else
			{
				if(tex->compressed)
					_gl.CompressedTexSubImage2DARB(tex->sides[side], static_cast<GLint>(level), static_cast<GLint>(x), static_cast<GLint>(y), static_cast<GLint>(tex->width), 1,
						tex->iformat, static_cast<GLsizei>(numBytes), data);
				else
					_gl.TexSubImage2D(tex->sides[side], static_cast<GLint>(level), static_cast<GLint>(x), static_cast<GLint>(y), static_cast<GLint>(tex->width), 1,
						tex->exchannels, tex->exformat, data);
			}
		}

		void GLRenderDevice::SetTextureMaxLOD(RenderTextureHandle rth, UInt32 level)
		{
			GLTexture *tex = GLHandleToRef<GLTexture>(rth);
			_state.BindTexture(tex->target, tex->texID);
			_gl.TexParameteri(_state.TextureTarget(tex->target), GL_TEXTURE_MAX_LOD, static_cast<GLint>(level));
		}
		
		void GLRenderDevice::SetTextureMinLOD(RenderTextureHandle rth, UInt32 level)
		{
			GLTexture *tex = GLHandleToRef<GLTexture>(rth);
			_state.BindTexture(tex->target, tex->texID);
			_gl.TexParameteri(_state.TextureTarget(tex->target), GL_TEXTURE_MIN_LOD, static_cast<GLint>(level));
		}

		bool GLRenderDevice::DestroyTexture(RenderTextureHandle rth)
		{
			if(rth == 0)
				return false;
			GLTexture *tex = GLHandleToRef<GLTexture>(rth);

			_gl.DeleteTextures(1, &tex->texID);
			_alloc.Free(tex);
			return true;
		}

		bool GLRenderDevice::CreateShader(Resources::ShaderSpec::ShaderType shaderType, const void *data, LargeUInt sizeBytes, RenderShaderHandle *outRSH)
		{
			GLShader *shd = _alloc.CAlloc<GLShader>(1);
			GLenum glslShaderType = 0;
			if(!shd)
				goto error;

			shd->isCompiled = false;
			shd->shaderType = shaderType;
			shd->shaderID = 0;
			shd->isGLSL = false;

			switch(shaderType)
			{
			case ShaderSpec::ST_GLSL_Pixel:
				glslShaderType = GL_FRAGMENT_SHADER_ARB;
				break;
			case ShaderSpec::ST_GLSL_Vertex:
				glslShaderType = GL_VERTEX_SHADER_ARB;
				break;
			default:
				goto error;
			}

			if(glslShaderType)
			{
				shd->isGLSL = true;
				const GLcharARB *progSource = static_cast<const GLcharARB *>(data);
				GLint len = sizeBytes;
				if(len < 1 || static_cast<LargeUInt>(len) != sizeBytes)
					goto error;		// Overflow or something
				shd->shaderID = _gl.CreateShaderObjectARB(glslShaderType);
				if(!shd->shaderID)
					goto error;
				_gl.ShaderSourceARB(shd->shaderID, 1, &progSource, &len);
				_gl.CompileShaderARB(shd->shaderID);
				GLint compileStatus;
				_gl.GetObjectParameterivARB(shd->shaderID, GL_OBJECT_COMPILE_STATUS_ARB, &compileStatus);
				FILE *f = fopen("srcdump.glsl", "wb");
				fwrite(progSource, len, 1, f);
				fclose(f);
				
				if(compileStatus != GL_TRUE)
				{
					GLint errBufSize;
					_gl.GetObjectParameterivARB(shd->shaderID, GL_OBJECT_INFO_LOG_LENGTH_ARB, &errBufSize);
					GLsizei actualLen;
					char *errBuf = new char[errBufSize];
					_gl.GetInfoLogARB(shd->shaderID, errBufSize, &actualLen, errBuf);
					FILE *f = fopen("glslerr.txt", "wb");
					fwrite(errBuf, actualLen, 1, f);
					fclose(f);
					delete[] errBuf;

					goto error;
				}

				shd->isCompiled = true;
			}

			*outRSH = reinterpret_cast<const UInt8 *>(shd) - reinterpret_cast<const UInt8 *>(NULL);
			return true;

		error:
			if(shd->shaderID != 0)
			{
				shd->shaderID = 0;
				_gl.DeleteObjectARB(shd->shaderID);
			}
			if(shd != NULL)
				_alloc.Free(shd);
			*outRSH = 0;
			return false;
		}

		void GLRenderDevice::DestroyShader(RenderShaderHandle rsh)
		{
			if(rsh == 0)
				return;
			GLShader *shd = GLHandleToRef<GLShader>(rsh);

			if(shd->isGLSL)
			{
				if(shd->shaderID != 0)
					_gl.DeleteObjectARB(shd->shaderID);
				shd->shaderID = 0;
			}

			_alloc.Free(shd);
		}


		GLRenderDevice::~GLRenderDevice()
		{
		}
	}
}